Skip to content

feat: custom dashboard views#36

Merged
TerrifiedBug merged 7 commits intomainfrom
feat/custom-dashboards
Mar 7, 2026
Merged

feat: custom dashboard views#36
TerrifiedBug merged 7 commits intomainfrom
feat/custom-dashboards

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

  • Add DashboardView Prisma model with migration for storing user-scoped custom dashboard views (panels, filters, sort order)
  • Add CRUD tRPC procedures (listViews, createView, updateView, deleteView) to the dashboard router with ownership checks
  • Add tab bar to the dashboard page for switching between the default view and custom views, with inline edit/delete controls
  • Add ViewBuilderDialog component with panel selection (10 panels across Pipeline, System, and Summary categories)
  • Add CustomView component that renders selected panels in a responsive 2-column grid with the same time range picker and filter bar
  • Update public docs with a section on creating, switching, editing, and deleting custom views

Test plan

  • Run npx prisma migrate dev against a database to apply the migration
  • Verify the Default tab displays the original dashboard unchanged
  • Create a custom view with a subset of panels and verify only those panels render
  • Edit a custom view (rename + change panels) and verify changes persist
  • Delete a custom view and verify it is removed from the tab bar
  • Confirm views are user-scoped: log in as a different user and verify they do not see other users' views
  • Verify filter bar and time range picker work within custom views
  • Verify responsive layout at different breakpoints

Add Prisma schema model and migration for user-scoped custom dashboard
views with panel selection, optional filters, and sort ordering.
Add listViews, createView, updateView, and deleteView tRPC procedures
to the dashboard router for managing user-scoped custom dashboard views.
Add a tab bar at the top of the dashboard for switching between the
default view and user-created custom views. Includes a view builder
dialog for selecting panels and a custom view component that renders
the chosen charts and summary cards in a responsive grid.
Document the custom view workflow: creating, switching, editing, and
deleting views, with a stepper guide and panel reference table.
@github-actions github-actions bot added feature documentation Improvements or additions to documentation and removed feature labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR adds a user-scoped custom dashboard views feature to the existing dashboard page, allowing users to persist named subsets of dashboard panels with optional filter presets. The implementation covers the full stack: a new DashboardView Prisma model with migration, four new tRPC procedures, a tab bar UI on the dashboard page, a ViewBuilderDialog for creating/editing views, and a CustomView component that fetches only the data required by the selected panels.

Key observations:

  • The previously flagged issues (missing withAudit, onSuccess resetting active view on any delete, and the sortOrder TOCTOU race) have all been addressed in this revision.
  • All three write mutations (createView, updateView, deleteView) still use .use(withTeamAccess("VIEWER")) rather than the correct "EDITOR" role required by the project's RBAC policy for create/modify operations — this was flagged in a prior review thread and remains unresolved.
  • The delete handler in page.tsx uses selectedEnvironmentId! (a non-null assertion) instead of the ?? "" fallback used by the create and update paths, which will produce a runtime BAD_REQUEST when a user attempts to delete a view with no environment currently selected.

Confidence Score: 2/5

  • Not safe to merge — write mutations use the wrong RBAC role and the delete handler has a null-safety bug that produces a runtime error.
  • Two concrete, unresolved correctness issues remain: (1) all three write mutations gate on VIEWER instead of EDITOR, violating the project's role hierarchy contract; (2) the delete mutation passes a potentially-null environmentId due to a non-null assertion, causing a guaranteed Zod validation failure when no environment is selected. Both issues will manifest under normal usage.
  • src/app/(dashboard)/page.tsx (null-safety bug in delete handler) and src/server/routers/dashboard.ts (incorrect RBAC role on all three write mutations).

Important Files Changed

Filename Overview
src/app/(dashboard)/page.tsx Adds custom view tab bar and delete mutation; contains a non-null assertion (selectedEnvironmentId!) in the delete handler that passes null to Zod's z.string() validator when no environment is selected, causing a BAD_REQUEST error at runtime.
src/server/routers/dashboard.ts Adds four new tRPC procedures for CRUD operations on DashboardView; withAudit middleware is present on all three write mutations; all three write mutations (createView, updateView, deleteView) use withTeamAccess("VIEWER") rather than withTeamAccess("EDITOR"), meaning read-only team members can freely create, rename, and delete dashboard views.
src/components/dashboard/custom-view.tsx Renders selected panels in a responsive grid, conditionally fetches only required data, and correctly derives filter defaults from saved view state. No issues found.
src/components/dashboard/view-builder-dialog.tsx Dialog for creating and editing custom views; form state resets correctly on each open via conditional rendering; mutation invalidation and close handling are correct.
prisma/schema.prisma Adds DashboardView model with correct CUID primary key, cascade delete on user foreign key, and userId index. Migration SQL matches the schema definition.
prisma/migrations/20260307100000_add_dashboard_views/migration.sql Creates DashboardView table with correct column types (JSONB for panels/filters), index on userId, and cascading FK to User. No issues found.
docs/public/user-guide/dashboard.md Replaces the Data Volume Analytics section with documentation for the new custom dashboard views feature. Content accurately reflects the implementation.

Sequence Diagram

sequenceDiagram
    participant U as User (Browser)
    participant P as page.tsx
    participant VB as ViewBuilderDialog
    participant CV as CustomView
    participant R as dashboard router (tRPC)
    participant DB as PostgreSQL

    U->>P: Load dashboard
    P->>R: listViews (protectedProcedure)
    R->>DB: SELECT * FROM DashboardView WHERE userId = ?
    DB-->>R: DashboardView[]
    R-->>P: views[]
    P-->>U: Render tab bar (Default + custom tabs)

    U->>P: Click "+ New View"
    P->>VB: open=true
    U->>VB: Enter name + select panels → Save
    VB->>R: createView (VIEWER + withAudit)
    R->>DB: aggregate(_max sortOrder) → INSERT DashboardView
    DB-->>R: new DashboardView
    R-->>VB: created view
    VB->>P: invalidateQueries listViews + close

    U->>P: Click custom view tab
    P->>CV: render CustomView(view)
    CV->>R: chartMetrics / stats / pipelineCards (conditional)
    R->>DB: query metrics
    DB-->>R: data
    R-->>CV: panel data
    CV-->>U: Render selected panels

    U->>P: Click delete icon → confirm
    P->>R: deleteView (VIEWER + withAudit)
    R->>DB: SELECT + DELETE DashboardView WHERE id = ?
    DB-->>R: ok
    R-->>P: {deleted: true}
    P->>P: invalidateQueries + conditionally reset activeView
Loading

Last reviewed commit: 9998bfc

Previously, deleting any custom view would unconditionally switch back
to the Default tab. Now only resets if the deleted view was active.
- Replace count-based sortOrder with aggregate max to avoid TOCTOU race
- Add withTeamAccess("VIEWER") to createView, updateView, deleteView
- Add withAudit middleware to all three view mutations
- Pass environmentId through frontend for team context resolution
@TerrifiedBug TerrifiedBug merged commit 64f4fb9 into main Mar 7, 2026
3 of 4 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/custom-dashboards branch March 7, 2026 14:44
onClick={(e) => {
e.stopPropagation();
if (confirm(`Delete "${view.name}"?`)) {
deleteMutation.mutate({ environmentId: selectedEnvironmentId!, id: view.id });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-null assertion passes null to Zod z.string() schema

selectedEnvironmentId is typed as string | null | undefined from the environment store. Using selectedEnvironmentId! here does not coerce null to a string — TypeScript's ! only suppresses the compiler's null check. At runtime, if no environment is selected, null is passed as the environmentId and Zod's z.string() validator will reject it with a BAD_REQUEST, causing a silent mutation failure.

The create and update flows correctly handle this by using selectedEnvironmentId ?? "" (e.g., lines 419 and 426). The same fallback should be applied here for consistency:

Suggested change
deleteMutation.mutate({ environmentId: selectedEnvironmentId!, id: view.id });
deleteMutation.mutate({ environmentId: selectedEnvironmentId ?? "", id: view.id });

Because listViews requires no environment context, it's possible for a user to have the tab bar rendered with views while selectedEnvironmentId is still null, making this a realistic failure path rather than a theoretical one.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/(dashboard)/page.tsx
Line: 190

Comment:
**Non-null assertion passes `null` to Zod `z.string()` schema**

`selectedEnvironmentId` is typed as `string | null | undefined` from the environment store. Using `selectedEnvironmentId!` here does not coerce `null` to a string — TypeScript's `!` only suppresses the compiler's null check. At runtime, if no environment is selected, `null` is passed as the `environmentId` and Zod's `z.string()` validator will reject it with a `BAD_REQUEST`, causing a silent mutation failure.

The create and update flows correctly handle this by using `selectedEnvironmentId ?? ""` (e.g., lines 419 and 426). The same fallback should be applied here for consistency:

```suggestion
                    deleteMutation.mutate({ environmentId: selectedEnvironmentId ?? "", id: view.id });
```

Because `listViews` requires no environment context, it's possible for a user to have the tab bar rendered with views while `selectedEnvironmentId` is still `null`, making this a realistic failure path rather than a theoretical one.

How can I resolve this? If you propose a fix, please make it concise.

TerrifiedBug added a commit that referenced this pull request Mar 7, 2026
The merge of custom dashboards (#36) and live tail (#30) created a
conflict where LiveTailPanel references `componentKey` which was never
destructured as a local variable. The correct reference is `storeKey`,
derived from selectedNode.data.componentKey at the top of the component.
TerrifiedBug added a commit that referenced this pull request Mar 7, 2026
* fix: use storeKey instead of undefined componentKey in LiveTailPanel

The merge of custom dashboards (#36) and live tail (#30) created a
conflict where LiveTailPanel references `componentKey` which was never
destructured as a local variable. The correct reference is `storeKey`,
derived from selectedNode.data.componentKey at the top of the component.

* docs: update OIDC group sync documentation

Rewrite the OIDC role/team mapping section to reflect the new group
sync toggle, separate scope/claim fields, and stepper-based setup
guide. Removes references to legacy Admin/Editor Groups fields.
TerrifiedBug added a commit that referenced this pull request Mar 7, 2026
…42)

The merge of custom dashboards (#36) and live tail (#30) created a
conflict where LiveTailPanel references `componentKey` which was never
destructured as a local variable. The correct reference is `storeKey`,
derived from selectedNode.data.componentKey at the top of the component.
TerrifiedBug added a commit that referenced this pull request Mar 7, 2026
…42)

The merge of custom dashboards (#36) and live tail (#30) created a
conflict where LiveTailPanel references `componentKey` which was never
destructured as a local variable. The correct reference is `storeKey`,
derived from selectedNode.data.componentKey at the top of the component.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant